If consumers had kept their baskets fixed each period, the index would have risen by this much
Weight Component
-0.4
-0.5
Consumers shifted toward Private insurance. This is the “substitution” effect. This partially offset price increases.
Total
+1.9
+1.9
Total change in the price index between \(t-k\) and \(t\)
Key Conceptual Differences
Type
Laspeyres
Paasche
Price effect evaluated at
Old weights
New weights
Weight effect evaluated at
New prices
Old prices
Emphasizes
What inflation would have been without behavioral change
What inflation was experienced given consumption behavior
For our project, Laspeyres is useful if we’re trying to answer: “How much did hospital input cost rise, holding service mix constant?” - isolating pure cost pressures. Paasche might be useful if we’re asking “Given how hospitals changed their mix, what drove observed spending growth?” - capturing the realized experience.
By-Payer Decomposition
Note, we can also take the work we have done to aggregate by payer type. Recall the two expressions we created for the Laspeyres Decomposition as an example.
We will read the expenditure share data from the link CMS NHEA. Since the PCE Price Index does not publish expenditure shares by payer type, it is necessary to obtain weights from elsewhere. These will be found in the Center for Medicaid and Medicare Services (CMS) National Health Expenditure Accounts (NHEA) tables published online. You can find the relevant table at this link CMS NHEA. After downloading, we will read Table 7 which contains the expenditure shares by payer type. From this table, we will create the following variables:
Medicaid = G5:G32
Medicare = F5:F32
Other = C5:C32 + E5:E32 + H5:H32 + I5:I32 = Out of Pocket Payers + Private Health Insurance + Other Health Insurance + Other Third Party Payers
We create the Other variable to match the “Private and all other patients” designation of the data series with Haver Code R62211A6. We verify that the total column (B5:B32) is the sum total of these 3 categories (up to rounding error). We re-calculate this total value by summing the 3 variables above to ensure that the expenditure shares of each payer-type sum to 1.
Code
# check to see if the directory existsif (!dir.exists(here('data/cms'))) {dir.create(here('data/cms'))}# check to see if the file is downloadedif (!file.exists(here('data/cms/nhe-tables.zip'))) {download.file(url ='https://www.cms.gov/files/zip/nhe-tables.zip',destfile =here('data/cms/nhe-tables.zip'),mode ='wb' )unzip(zipfile =here("data/cms", "nhe-tables.zip"),exdir =here("data/cms", "nhe-tables") )}# read medicare and medicaid expendituresmedicare = readxl::read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), range ='F4:F32', ) |>as.matrix() |>as.vector()# read medicare and medicaid expendituresmedicaid = readxl::read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), range ='G4:G32', ) |>as.matrix() |>as.vector()# read private private = readxl::read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), range ='E4:E32', ) |>as.matrix() |>as.vector()# read out of pocket oop = readxl::read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), range ='C4:C32', ) |>as.matrix() |>as.vector()# read other health insurance other_health_ins =read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), range ='H4:H32', ) |>as.matrix() |>as.vector()# read other 3rd party payers third_party =read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), range ='I4:I32', ) |>as.matrix() |>as.vector()# combine all of these not-medicare, not-medicaid variables into one "other" category other = private + oop + other_health_ins + third_party# read total expenditures total =read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), range ='B4:B32') |>as.matrix() |>as.vector()# store year year =read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), range ='A4:A32')[,1] |>as.matrix() |>as.vector()# combine into one data frameexp =data.frame(year = year, total = total, Medicare = medicare, Medicaid = medicaid, Other = other) |>mutate(across(where(is.numeric), ~round(., 1)) ) |>mutate(total = Medicare + Medicaid + Other ) |>filter(year >=2000)
Total Expenditures by Payer-Type (Annual)
Code
# note: the total matches them to rounding error. # plot exp |>rename(`Private/Other`= Other) |>select(-total) |>pivot_longer(cols =!year, names_to ='Series', values_to ='Value' ) |>ggplot(aes(x = year, color = Series, y = Value)) +# geom_point(size = 3) +geom_line(linewidth =1.05) +labs(y ='Hospital Expenditures',x ='Year',subtitle ='Billions $' ) +scale_color_manual(values = ggpubr::get_palette('jco', 4)) +scale_x_continuous(breaks =seq(min(exp$year), max(exp$year), by =2))
The expenditure data are annual here. We will change this to monthly data by creating a sequence of monthly dates from January 2000 to December 2024, storing the year of each month, and merging the annual expenditure data using this year variable.
We repeat the same exercise at the annual frequency. We compute expenditure shares from the annual CMS data directly (rather than spreading them across months) and take the annual average of the monthly PPI series. The weighted price index \(P_t = \sum_i w_{i,t} \times p_{i,t}\) is then computed year-by-year, and year-over-year inflation is \(\pi_t = 100 \times (P_t - P_{t-1}) / P_{t-1}\).
legend = ggpubr::get_palette('jco', 2)names(legend) =c('Price Component', 'Weight Component')inner_join(exp_monthly_long, ppi_long, by =c('date', 'payerType')) |># get price component of the laspeyres-style decompgroup_by(payerType) |>mutate(price_component_L =lag(wt, 12) * (ppi -lag(ppi, 12)),weight_component_L = ppi * (wt -lag(wt, 12)) ) |>ungroup() |>group_by(date) |>summarise(`Price Component`=sum(price_component_L, na.rm = T),`Weight Component`=sum(weight_component_L, na.rm = T) ) |>ungroup() |># normalize by P_{t-k} to convert from level change to percentage changeleft_join(inflation, by ='date') |>mutate(`Price Component`=`Price Component`/lag(P_t, 12) *100,`Weight Component`=`Weight Component`/lag(P_t, 12) *100,pi_t = pi_t ) |>pivot_longer(cols =c(`Price Component`, `Weight Component`),names_to ='Component',values_to ='Contribution' ) |># check the inflation total matches pi_tgroup_by(date) |>mutate(check =sum(Contribution)) |>ungroup() |>mutate(flag =ifelse(round(check - pi_t, 5) ==0, F, T)) |>filter(date >='2001-01-01') |># summary()ggplot(aes(x = date)) +geom_col(aes(y = Contribution, color = Component, fill = Component), alpha = .7) +geom_point(aes(y = pi_t)) +geom_line(aes(y = pi_t), linetype ='solid') +geom_hline(yintercept =0, linetype ='solid') +scale_color_manual(values = legend) +scale_fill_manual(values = legend) +scale_x_date(breaks =seq(min(exp_monthly$date), max(exp_monthly$date), by ="2 years"),date_labels ='%Y') +labs(x ='Month', y ='Percentage Point')
Code
legend = ggpubr::get_palette('jco', 3)names(legend) =c('Private/Other', 'Medicare', 'Medicaid')inner_join(exp_monthly_long, ppi_long, by =c('date', 'payerType')) |>mutate(payerType =ifelse(payerType =='Other', 'Private/Other', payerType) ) |># get price component of the laspeyres-style decompgroup_by(payerType) |>mutate(price_component_L =lag(wt, 12) * (ppi -lag(ppi, 12)),weight_component_L = ppi * (wt -lag(wt, 12)) ) |>ungroup() |># store the contribution from each mutate(contribution_L = price_component_L + weight_component_L ) |>select( date, payerType, contribution_L ) |>arrange(payerType, date) |># normalize by P_{t-k} to convert from level change to percentage changeleft_join(inflation, by ='date') |>group_by(payerType) |>mutate(Contribution = contribution_L /lag(P_t, 12) *100,pi_t = pi_t ) |>ungroup() |># check the inflation total matches pi_tgroup_by(date) |>mutate(check =sum(Contribution)) |>ungroup() |>mutate(flag =ifelse(round(check - pi_t, 5) ==0, F, T) ) |>filter(date >='2001-01-01') |>ggplot(aes(x = date)) +geom_col(aes(y = Contribution, color = payerType, fill = payerType), alpha = .7) +geom_point(aes(y = pi_t)) +geom_line(aes(y = pi_t), linetype ='solid') +geom_hline(yintercept =0, linetype ='solid') +scale_color_manual(values = legend) +scale_fill_manual(values = legend) +scale_x_date(breaks =seq(min(exp_monthly$date), max(exp_monthly$date), by ="2 years"),date_labels ='%Y') +labs(x ='Month', y ='Percentage Points',fill ='Payer Type', color ='Payer Type' )
Code
legend = ggpubr::get_palette('jco', 2)names(legend) =c('Price Component', 'Weight Component')inner_join(exp_monthly_long, ppi_long, by =c('date', 'payerType')) |># get price component of the paasche-style decompgroup_by(payerType) |>mutate(price_component_P = wt * (ppi -lag(ppi, 12)),weight_component_P =lag(ppi, 12) * (wt -lag(wt, 12)) ) |>ungroup() |>group_by(date) |>summarise(`Price Component`=sum(price_component_P, na.rm = T),`Weight Component`=sum(weight_component_P, na.rm = T) ) |>ungroup() |># normalize by P_{t-k} to convert from level change to percentage changeleft_join(inflation, by ='date') |>mutate(`Price Component`=`Price Component`/lag(P_t, 12) *100,`Weight Component`=`Weight Component`/lag(P_t, 12) *100,pi_t = pi_t ) |>pivot_longer(cols =c(`Price Component`, `Weight Component`),names_to ='Component',values_to ='Contribution' ) |># check the inflation total matches pi_tgroup_by(date) |>mutate(check =sum(Contribution)) |>ungroup() |>mutate(flag =ifelse(round(check - pi_t, 5) ==0, F, T) ) |>filter(date >='2001-01-01') |># summary()ggplot(aes(x = date)) +geom_col(aes(y = Contribution, color = Component, fill = Component), alpha = .7) +geom_point(aes(y = pi_t)) +geom_line(aes(y = pi_t), linetype ='solid') +geom_hline(yintercept =0, linetype ='solid') +scale_color_manual(values = legend) +scale_fill_manual(values = legend) +scale_x_date(breaks =seq(min(exp_monthly$date), max(exp_monthly$date), by ="2 years"),date_labels ='%Y') +labs(x ='Month', y ='Percentage Point' )
Code
legend = ggpubr::get_palette('jco', 3)names(legend) =c('Private/Other', 'Medicare', 'Medicaid')inner_join(exp_monthly_long, ppi_long, by =c('date', 'payerType')) |>mutate(payerType =ifelse(payerType =='Other', 'Private/Other', payerType) ) |># get price component of the paasche-style decompgroup_by(payerType) |>mutate(price_component_P = wt * (ppi -lag(ppi, 12)),weight_component_P =lag(ppi, 12) * (wt -lag(wt, 12)) ) |>ungroup() |># store the contribution from each mutate(contribution_P = price_component_P + weight_component_P ) |>select( date, payerType, contribution_P ) |>arrange(payerType, date) |># normalize by P_{t-k} to convert from level change to percentage changeleft_join(inflation, by ='date') |>group_by(payerType) |>mutate(Contribution = contribution_P /lag(P_t, 12) *100,pi_t = pi_t ) |>ungroup() |># check the inflation total matches pi_tgroup_by(date) |>mutate(check =sum(Contribution)) |>ungroup() |>mutate(flag =ifelse(round(check - pi_t, 5) ==0, F, T) ) |>filter(date >='2001-01-01') |>ggplot(aes(x = date)) +geom_col(aes(y = Contribution, color = payerType, fill = payerType), alpha = .7) +geom_point(aes(y = pi_t)) +geom_line(aes(y = pi_t), linetype ='solid') +geom_hline(yintercept =0, linetype ='solid') +scale_color_manual(values = legend) +scale_fill_manual(values = legend) +scale_x_date(breaks =seq(min(exp_monthly$date), max(exp_monthly$date), by ="2 years"),date_labels ='%Y') +labs(x ='Month', y ='Percentage Points',fill ='Payer Type', color ='Payer Type' )
Industry Based Decomposition of Hospital Inflation (Annual)
We now decompose annual hospital inflation into price and weight components using the same Laspeyres and Paasche identities applied to the annual data. The lag is 1 year rather than 12 months. Each component is normalized by \(P_{t-1}\) and multiplied by 100 so the contributions are in percentage points and sum to \(\pi_t\).
The Laspeyres decomposition evaluates price changes at prior-year weights and weight changes at current-year prices: \(\Delta P_t^L = \sum_i w_{i,t-1} \Delta p_i + \sum_i \Delta w_i \, p_{i,t}\).
Code
legend = ggpubr::get_palette('jco', 2)names(legend) =c('Price Component', 'Weight Component')inner_join(exp_year_long, ppi_yearly_long, by =c('year', 'payerType')) |># get price component of the laspeyres-style decompgroup_by(payerType) |>mutate(price_component_L =lag(wt, 1) * (ppi -lag(ppi, 1)),weight_component_L = ppi * (wt -lag(wt, 1)) ) |>ungroup() |>group_by(year) |>summarise(`Price Component`=sum(price_component_L, na.rm = T),`Weight Component`=sum(weight_component_L, na.rm = T) ) |>ungroup() |># normalize by P_{t-k} to convert from level change to percentage changeleft_join(inflation_annual, by ='year') |>mutate(`Price Component`=`Price Component`/lag(P_t, 1) *100,`Weight Component`=`Weight Component`/lag(P_t, 1) *100,pi_t = pi_t ) |>pivot_longer(cols =c(`Price Component`, `Weight Component`),names_to ='Component',values_to ='Contribution' ) |># check the inflation total matches pi_tgroup_by(year) |>mutate(check =sum(Contribution)) |>ungroup() |>mutate(flag =ifelse(round(check - pi_t, 5) ==0, F, T) ) |>filter(year >=2001) |># summary()ggplot(aes(x = year)) +geom_col(aes(y = Contribution, color = Component, fill = Component), alpha = .7) +geom_point(aes(y = pi_t)) +geom_line(aes(y = pi_t), linetype ='solid') +geom_hline(yintercept =0, linetype ='solid') +scale_color_manual(values = legend) +scale_fill_manual(values = legend) +scale_x_continuous(breaks =seq(min(exp_year_long$year), max(exp_year_long$year), by =2) ) +labs(x ='Year', y ='Percentage Point' )
Rather than splitting into price vs. weight, we aggregate each payer’s total contribution (price + weight) to show how much of overall inflation is attributable to Medicare, Medicaid, and Private/Other.
Code
legend = ggpubr::get_palette('jco', 3)names(legend) =c('Private/Other', 'Medicare', 'Medicaid')inner_join(exp_year_long, ppi_yearly_long, by =c('year', 'payerType')) |>mutate(payerType =ifelse(payerType =='Other', 'Private/Other', payerType)) |># get price component of the laspeyres-style decompgroup_by(payerType) |>mutate(price_component_L =lag(wt, 1) * (ppi -lag(ppi, 1)),weight_component_L = ppi * (wt -lag(wt, 1)) ) |>ungroup() |># store the contribution from eachmutate(contribution_L = price_component_L + weight_component_L ) |>select( year, payerType, contribution_L ) |>arrange(payerType, year) |># normalize by P_{t-k} to convert from level change to percentage changeleft_join(inflation_annual, by ='year') |>group_by(payerType) |>mutate(Contribution = contribution_L /lag(P_t, 1) *100,pi_t = pi_t ) |>ungroup() |># check the inflation total matches pi_tgroup_by(year) |>mutate(check =sum(Contribution)) |>ungroup() |>mutate(flag =ifelse(round(check - pi_t, 5) ==0, F, T) ) |>filter(year >=2001) |>ggplot(aes(x = year)) +geom_col(aes(y = Contribution, color = payerType, fill = payerType), alpha = .7) +geom_point(aes(y = pi_t)) +geom_line(aes(y = pi_t), linetype ='solid') +geom_hline(yintercept =0, linetype ='solid') +scale_color_manual(values = legend) +scale_fill_manual(values = legend) +scale_x_continuous(breaks =seq(min(exp_year_long$year), max(exp_year_long$year), by =2) ) +labs(x ='Year', y ='Percentage Points',fill ='Payer Type', color ='Payer Type' )
The Paasche decomposition evaluates price changes at current-year weights and weight changes at prior-year prices: \(\Delta P_t^P = \sum_i w_{i,t} \Delta p_i + \sum_i \Delta w_i \, p_{i,t-1}\).
Code
legend = ggpubr::get_palette('jco', 2)names(legend) =c('Price Component', 'Weight Component')inner_join(exp_year_long, ppi_yearly_long, by =c('year', 'payerType')) |># get price component of the paasche-style decompgroup_by(payerType) |>mutate(price_component_P = wt * (ppi -lag(ppi, 1)),weight_component_P =lag(ppi, 1) * (wt -lag(wt, 1)) ) |>ungroup() |>group_by(year) |>summarise(`Price Component`=sum(price_component_P, na.rm = T),`Weight Component`=sum(weight_component_P, na.rm = T) ) |>ungroup() |># normalize by P_{t-k} to convert from level change to percentage changeleft_join(inflation_annual, by ='year') |>mutate(`Price Component`=`Price Component`/lag(P_t, 1) *100,`Weight Component`=`Weight Component`/lag(P_t, 1) *100,pi_t = pi_t ) |>pivot_longer(cols =c(`Price Component`, `Weight Component`),names_to ='Component',values_to ='Contribution' ) |># check the inflation total matches pi_tgroup_by(year) |>mutate(check =sum(Contribution)) |>ungroup() |>mutate(flag =ifelse(round(check - pi_t, 5) ==0, F, T) ) |>filter(year >=2001) |># summary()ggplot(aes(x = year)) +geom_col(aes(y = Contribution, color = Component, fill = Component), alpha = .7) +geom_point(aes(y = pi_t)) +geom_line(aes(y = pi_t), linetype ='solid') +geom_hline(yintercept =0, linetype ='solid') +scale_color_manual(values = legend) +scale_fill_manual(values = legend) +scale_x_continuous(breaks =seq(min(exp_year_long$year), max(exp_year_long$year), by =2) ) +labs(x ='Year', y ='Percentage Point' )
Same payer-level aggregation as the Laspeyres tab, but using the Paasche identity so that price effects are evaluated at current weights and weight effects at prior-year prices.
Code
legend = ggpubr::get_palette('jco', 3)names(legend) =c('Private/Other', 'Medicare', 'Medicaid')inner_join(exp_year_long, ppi_yearly_long, by =c('year', 'payerType')) |>mutate(payerType =ifelse(payerType =='Other', 'Private/Other', payerType)) |># get price component of the paasche-style decompgroup_by(payerType) |>mutate(price_component_P = wt * (ppi -lag(ppi, 1)),weight_component_P =lag(ppi, 1) * (wt -lag(wt, 1)) ) |>ungroup() |># store the contribution from eachmutate(contribution_P = price_component_P + weight_component_P ) |>select( year, payerType, contribution_P ) |>arrange(payerType, year) |># normalize by P_{t-k} to convert from level change to percentage changeleft_join(inflation_annual, by ='year') |>group_by(payerType) |>mutate(Contribution = contribution_P /lag(P_t, 1) *100,pi_t = pi_t ) |>ungroup() |># check the inflation total matches pi_tgroup_by(year) |>mutate(check =sum(Contribution)) |>ungroup() |>mutate(flag =ifelse(round(check - pi_t, 5) ==0, F, T) ) |>filter(year >=2001) |>ggplot(aes(x = year)) +geom_col(aes(y = Contribution, color = payerType, fill = payerType), alpha = .7) +geom_point(aes(y = pi_t)) +geom_line(aes(y = pi_t), linetype ='solid') +geom_hline(yintercept =0, linetype ='solid') +scale_color_manual(values = legend) +scale_fill_manual(values = legend) +scale_x_continuous(breaks =seq(min(exp_year_long$year), max(exp_year_long$year), by =2)) +labs(x ='Year',y ='Percentage Points',fill ='Payer Type',color ='Payer Type')
Commodity-Based Hospital Inflation
Producer Price Index (PPI)
In the commodity-based categorization of the PPI, there are categories for “Hospital outpatient care” and “Hospital inpatient care”, each of which have a subcategory for “General medical and surgical hospitals”. This subcategory for general hospitals has price indexes by payer type, just as detailed for the previous method. The overall categorization looks like:
Hospital outpatient care
a. General medical and surgical hospitals
Medicare patients
Medicaid patients
Private insurance and all other patients
Hospital inpatient care
a. General medical and surgical hospitals
Medicare patients
Medicaid patients
Private insurance and all other patients
We need to aggregate the indexes of the same payer type to calculate the overall hospital services price index by that payer.
if (!dir.exists(here('data/bls'))) {dir.create(here('data/bls'))}if (length(list.files(here('data/bls'))) ==0) {# store urls urls =c('https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2025.xlsx','https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2024.xlsx','https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2023.xlsx','https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2022.xlsx','https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2021.xlsx','https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2020.xlsx','https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2019.xlsx','https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2018.xlsx','https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2017.xlsx','https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2016.xlsx','https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2015.xlsx' )# web download for (i inseq_along(urls)) {download.file(url = urls[i],destfile =here(paste0('data/bls/', 'relimport_', str_replace_all(urls[i], 'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-', '') )),mode ='wb') }}# store file namesfiles =list.files(here('data/bls'), full.names = T)# store commodity base codescodes =c('5121010111',# medicare inpatient'5121010112',# medicaid inpatient'5121010113',# other inpatient'5111040111',# medicare outpatient'5111040112',# medicaid outpatient'5111040113'# other outpatient)weights =list()for (i inseq_along(files)) {# read the first file temp =read_xlsx(files[i], skip =3) |>filter(`Commodity code`%in% codes)# check that the file has relative importance weights for each code (6) check =nrow(temp) ==6if (!check) stop('missing expenditure shares -> inspect!')# convert to DT data.table::setDT(temp)# drop previous rel import from 2 years ago temp = temp[, -3]# store year temp$year =as.integer(str_replace_all(colnames(temp)[3], 'Relative importance December ', ''))# rename for claritycolnames(temp)[3] ='relativeImportance'# convert to numeric temp$relativeImportance =as.numeric(temp$relativeImportance)# normalize the relative imortance by the sum to get share of hospital expenditures temp$relativeImportance = temp$relativeImportance /sum(temp$relativeImportance)# reorder cols temp = temp |>select(year, everything())# store in list weights[[i]] = temp}# combine data frames weights =rbindlist(weights) |>rename(code =`Commodity code` )# create label data frame to merge label_df =data.frame(code = codes,Series =c('Medicare_in', 'Medicaid_in', 'Other_in', 'Medicare_out', 'Medicaid_out', 'Other_out' ))# merge in labels weights =left_join(weights, label_df, by ='code') |>select(-Index, -code)# plot weights |>ggplot(aes(x = year, color = Series, y = relativeImportance)) +# geom_point() + geom_line() +labs(y ='Expenditure Importance - Commodity Based',x ='Year' ) +scale_color_manual(values = ggpubr::get_palette('jco', 6))